<script lang="ts">
import { ref, onMounted, onUnmounted, reactive, computed, nextTick } from 'vue';
import useClipboard from 'vue-clipboard3';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import {
  EnterOutlined,
  CloseCircleOutlined,
  CopyOutlined,
  ReloadOutlined,
} from '@ant-design/icons-vue';
import { Textarea, message } from 'ant-design-vue';
import { useQuestionTemplate } from './components/LLMQuestionTemplate.js';
import {
  useDot,
  useClearDot,
  useScrollToBottom,
  useClearScrollToBottom,
  useWheelControlScrollToBottom,
} from './components/SetInterval.js';
import options from './components/LLMOptions.js';

const { toClipboard } = useClipboard();

export default {
  components: {
    'a-textarea': Textarea,
    EnterOutlined,
    CloseCircleOutlined,
    CopyOutlined,
    ReloadOutlined,
  },
};
</script>

<script setup lang="ts">
const loading = ref(false); // 是否正在加载（EventSource 持续接收数据时）
const thinking = ref(false); // 是否正在思考（调用 EventSource 但未接收到数据时）
const dots = ref(''); // 加载时的提示符（...）
const url = ref(''); // 后端 API 的 URL
const apiKey = ref(''); // 后端 API 的密钥
const result_dom = ref<HTMLElement | null>(null); // 答案区域的 DOM 引用
let sw_listener: any; // Service Worker 的监听器
const question = ref(''); // 用户输入的问题
let ctrl: any; // EventSource 的控制器
const isExplain = ref(false); // 是否处于解释模式
const result = reactive<
  {
    question: string;
    answer: string;
    stop: boolean;
    isExplain: boolean;
    copyed: boolean;
  }[]
>([]); // 存储问题和答案的数组

const lastResult = computed(() => result[result.length - 1]); // 获取最后一个结果

/**
 * 组件挂载时执行的逻辑
 */
onMounted(() => {
  // 获取 URL 和 API Key
  getUrlApiKey();

  // 监听来自 Service Worker 的消息
  sw_listener = (request: any) => {
    if (request.action === 'explain') {
      isExplain.value = true;
      question.value = request.text;
      askQuestion(null);
    }
  };

  chrome.runtime.onMessage.addListener(sw_listener);
});

/**
 * 组件卸载时执行的逻辑
 */
onUnmounted(() => {
  if (sw_listener) {
    chrome.runtime.onMessage.removeListener(sw_listener);
  }
  useClearDot(); // 清除加载提示符
  useClearScrollToBottom(); // 停止自动滚动到底部
});

/**
 * 开始提问
 * @param e - 键盘事件（可选）
 */
function askQuestion(e: KeyboardEvent | null) {
  if (question.value.trim() === '' || loading.value) return;

  // 忽略回车和空格键的默认行为
  if (e && (e.keyCode === 32 || e.keyCode === 13)) {
    e.preventDefault();
    isExplain.value = false;
  }

  thinking.value = true;
  loading.value = true;
  result.push({
    question: question.value,
    answer: '',
    stop: false,
    isExplain: isExplain.value,
    copyed: false,
  });
  useScrollToBottom(result_dom); // 自动滚动到底部
  getQuestionTemplate(); // 获取问题模板并发送请求
}

/**
 * 停止提问
 */
function stopQuestion() {
  ctrl?.abort(); // 中止 EventSource 请求
  if (lastResult.value) {
    lastResult.value.stop = true; // 标记当前问题为已停止
  }
  loading.value = false;
  thinking.value = false;
  useClearDot(); // 清除加载提示符
  useClearScrollToBottom(); // 停止自动滚动到底部
}

/**
 * 复制答案到剪贴板
 * @param answer - 要复制的答案
 */
async function copy(item: any) {
  try {
    item.copyed = true;
    await toClipboard(item.answer);
    message.info('复制成功 !');
  } catch (e) {
    console.error(e);
  }
}

/**
 * 重新提问
 * @param item - 要重新提问的问题对象
 */
function refresh(item: { question: string; stop: boolean }) {
  if (item.stop) return;
  question.value = item.question;
  result.pop(); // 移除最后一个结果
  askQuestion(null); // 重新提问
}

/**
 * 监听答案区域的滚动事件
 * @param e - 滚动事件
 */
function wheelHandler(e: WheelEvent) {
  useWheelControlScrollToBottom(e, result_dom, loading); // 控制是否自动滚动到底部
}

/**
 * 获取问题模板并发送请求
 */
async function getQuestionTemplate() {
  const template = await useQuestionTemplate(question.value);
  if (template) {
    sendMessageToLLM(template); // 发送请求到大模型
  }
}

/**
 * 从 Chrome 存储中获取 URL 和 API Key
 */
function getUrlApiKey() {
  chrome.storage.sync.get(['local_llm_url', 'local_llm_apiKey'], (result) => {
    url.value = result.local_llm_url || options.url;
    apiKey.value = result.local_llm_apiKey || options.apiKey;
  });
}

/**
 * 发送问题到大模型并处理响应
 * @param template - 问题模板
 */
async function sendMessageToLLM(template: string) {
  useDot(dots); // 显示加载提示符
  try {
    const data = { content: template };
    ctrl = new AbortController(); // 创建 AbortController 用于中止请求
    question.value = '';

    await fetchEventSource(url.value, {
      ...options.http,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
      signal: ctrl.signal,
      onopen: async (response) => {
        if (!response.ok) {
          throw new Error('Failed to open connection: ' + response.statusText);
        }
        lastResult.value.isExplain = isExplain.value; // 设置是否为解释模式
      },
      onmessage(event) {
        thinking.value = false;
        console.log(event);
        if (event.event !== 'finish') {
          if (event.data == '\\n') {
            result[result.length - 1].answer += '\n';
          } else {
            result[result.length - 1].answer += event.data;
          }
        } else {
          useClearDot();
          useClearScrollToBottom();
          ctrl.abort();
          loading.value = false;
          thinking.value = false;
        }
      },
      onclose() {
        useClearDot(); // 清除加载提示符
        useClearScrollToBottom(); // 停止自动滚动到底部
        loading.value = false;
      },
      onerror(e) {
        console.error(e.message);
        if (e.message.includes('Failed to fetch')) {
          message.error('网络错误，请检查Url地址和网络连接！');
        }
        if (e.message.includes('Unauthorized')) {
          message.error('认证错误，请检查apiKey！');
        }
        useClearDot(); // 清除加载提示符
        useClearScrollToBottom(); // 停止自动滚动到底部
        ctrl.abort(); // 中止请求
        loading.value = false;
        thinking.value = false;
      },
    });
  } catch (error) {
    ctrl.abort(); // 中止请求
  }
}
</script>

<template>
  <div class="app-container">
    <div ref="result_dom" class="result" @wheel="wheelHandler($event)">
      <div v-for="(item, index) in result" :key="index">
        <div class="question-container">
          <div class="result-question">{{ item.question }}</div>
        </div>
        <div class="answer-container">
          <div class="ai-name">
            <img src="./assets/robot.png" alt="Qwen2" />
            <span
              >{{ item.isExplain ? '解释' : 'Qwen2'
              }}<span v-show="thinking && index === result.length - 1"
                >{{ item.isExplain ? '' : '思考中' }} {{ dots }}</span
              ></span
            >
          </div>
          <v-md-preview :text="item.answer"></v-md-preview>
          <em class="status" v-if="item.stop">(已停止)</em>
          <div v-else-if="!loading" class="tools">
            <div
              title="复制"
              :class="[item.copyed ? 'copy-success' : 'copy']"
              @click="copy(item)"
            ></div>
            <div
              class="refresh"
              title="重试"
              v-if="index === result.length - 1"
              @click="refresh(item)"
            ></div>
          </div>
        </div>
      </div>
    </div>
    <div class="question-wrapper">
      <div class="question">
        <a-textarea
          v-model:value.trim="question"
          placeholder="有什么问题尽管问我"
          :auto-size="{ minRows: 1, maxRows: 2 }"
          :bordered="false"
          @pressEnter="askQuestion($event)"
        />
        <div class="blank" title="请输入" v-if="!question && !loading"></div>
        <div
          class="answering"
          title="停止"
          v-if="loading"
          @click="stopQuestion"
        ></div>
        <div
          title="提问"
          class="inputting"
          v-if="question && !loading"
          @click="askQuestion"
        ></div>
      </div>
    </div>
  </div>
</template>
<style>
.github-markdown-body {
  padding: 0px !important;
}
</style>
<style lang="scss" scoped>
.app-container {
  box-sizing: border-box;
  padding: 16px 0px;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;

  .result {
    padding: 0 16px;
    flex: 1 1 auto;
    box-sizing: border-box;
    overflow-y: auto;
    overflow-x: hidden;
    text-overflow: hidden;
    width: 100%;
    font-size: 14px;

    .question-container {
      display: flex;
      width: 100%;
      height: auto;
      margin-bottom: 20px;
      flex-direction: row;
      justify-content: end;

      .result-question {
        max-width: 90%;
        height: auto;
        overflow-wrap: break-word;
        padding: 10px;
        border-radius: 4px;
        line-height: 22px;
        font-weight: 400;
        font-size: 14px;
        background-color: #c6dfff;
        color: #333333;
      }
    }

    .answer-container {
      position: relative;
      display: flex;
      width: 100%;
      height: auto;
      margin-bottom: 24px;
      flex-direction: column;
      justify-content: start;

      .ai-name {
        margin-bottom: 10px;
        line-height: 20px;

        img {
          width: 24px;
          height: 22px;
          margin-right: 8px;
          vertical-align: middle;
        }

        span {
          vertical-align: top;
          color: #333333;
          font-size: 14px;
          font-weight: 600;
          line-height: 24px;
        }
      }

      .status {
        position: absolute;
        left: 0;
        bottom: -8px;
        font-size: 12px;
        color: #999999;
        margin: 0 0 0 2px;
      }

      .tools {
        position: absolute;
        left: -2px;
        bottom: -14px;

        > div {
          display: inline-block;
          width: 18px;
          height: 18px;
          background-position: 0% 0%;
          background-repeat: no-repeat;
          background-size: 100%;
          margin-right: 10px;
          cursor: pointer;
        }
        div.copy {
          background-image: url('./assets/icon-copy.png');
        }
        div.copy:hover {
          background-image: url('./assets/icon-copy-hover.png');
        }

        div.copy-success {
          background-image: url('./assets/icon-copy-success.png');
        }

        div.refresh {
          background-image: url('./assets/icon-refresh.png');
        }
        div.refresh:hover {
          background-image: url('./assets/icon-refresh-hover.png');
        }

        .anticon {
          font-size: 12px;
          color: #fff;
          margin: 0px 12px 0 0;

          :hover {
            color: #000;
            cursor: pointer;
          }
        }
      }

      &:hover {
        .tools {
          .anticon {
            color: #595656;
          }
        }
      }
    }
  }
  .question-wrapper {
    box-sizing: border-box;
    width: 100%;
    height: auto;
    padding: 0 16px;
    .question {
      position: relative;
      box-sizing: border-box;
      width: 100%;
      height: auto;
      border: 1px solid rgba(240, 240, 240, 1);
      padding: 8px 48px 8px 6px;
      margin: 14px 0 0 0;
      border-radius: 4px;
      box-shadow: 0px 2px 8px 0px rgba(19, 53, 107, 0.15);

      textarea {
        font-size: 16px;
      }

      > div {
        position: absolute;
        right: 8px;
        bottom: 8px;
        display: inline-block;
        width: 32px;
        height: 32px;
        background-position: 0% 0%;
        background-repeat: no-repeat;
        background-size: 100%;
        cursor: pointer;
      }
      div.blank {
        background-image: url('./assets/icon-disable.png');
      }
      div.answering {
        background-image: url('./assets/icon-stop.png');
      }
      div.answering:hover {
        background-image: url('./assets/icon-stop-hover.png');
      }
      div.inputting {
        background-image: url('./assets/icon-send.png');
      }
      div.inputting:hover {
        background-image: url('./assets/icon-send-hover.png');
      }
    }
  }
}
</style>
